/**
 * Copyright (C) 2006 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.inject.internal.util;

import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;

import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * Looks up line numbers for classes and their members.
 *
 * @author Chris Nokleberg
 */
final class LineNumbers {

    private final Class type;
    private final Map<String, Integer> lines = Maps.newHashMap();
    private String source;
    private int firstLine = Integer.MAX_VALUE;

    /**
     * Reads line number information from the given class, if available.
     *
     * @param type the class to read line number information from
     * @throws IllegalArgumentException if the bytecode for the class cannot be found
     * @throws java.io.IOException if an error occurs while reading bytecode
     */
    public LineNumbers(Class type) throws IOException {
        this.type = type;

        if (!type.isArray()) {
            InputStream in = type.getResourceAsStream("/" + type.getName().replace('.', '/') + ".class");
            if (in != null) {
                new ClassReader(in).accept(new LineNumberReader(), ClassReader.SKIP_FRAMES);
            }
        }
    }

    /**
     * Get the source file name as read from the bytecode.
     *
     * @return the source file name if available, or null
     */
    public String getSource() {
        return source;
    }

    /**
     * Get the line number associated with the given member.
     *
     * @param member a field, constructor, or method belonging to the class used during construction
     * @return the wrapped line number, or null if not available
     * @throws IllegalArgumentException if the member does not belong to the class used during
     * construction
     */
    public Integer getLineNumber(Member member) {
        Preconditions.checkArgument(type == member.getDeclaringClass(), "Member %s belongs to %s, not %s", member, member.getDeclaringClass(), type);
        return lines.get(memberKey(member));
    }

    /** Gets the first line number. */
    public int getFirstLine() {
        return firstLine == Integer.MAX_VALUE ? 1 : firstLine;
    }

    private String memberKey(Member member) {
        checkNotNull(member, "member");

        /*if[AOP]*/
        if (member instanceof Field) {
            return member.getName();

        } else if (member instanceof Method) {
            return member.getName() + org.objectweb.asm.Type.getMethodDescriptor((Method) member);

        } else if (member instanceof Constructor) {
            StringBuilder sb = new StringBuilder().append("<init>(");
            for (Class param : ((Constructor) member).getParameterTypes()) {
                sb.append(org.objectweb.asm.Type.getDescriptor(param));
            }
            return sb.append(")V").toString();

        } else {
            throw new IllegalArgumentException("Unsupported implementation class for Member, " + member.getClass());
        }
        /*end[AOP]*/
        /*if[NO_AOP]
        return "<NO_MEMBER_KEY>";
        end[NO_AOP]*/
    }

    private class LineNumberReader extends ClassVisitor {

        private int line = -1;
        private String pendingMethod;
        private String name;

        LineNumberReader() {
            super(Opcodes.ASM5);
        }

        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            this.name = name;
        }

        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            if ((access & Opcodes.ACC_PRIVATE) != 0) {
                return null;
            }
            pendingMethod = name + desc;
            line = -1;
            return new LineNumberMethodVisitor();
        }

        public void visitSource(String source, String debug) {
            LineNumbers.this.source = source;
        }

        public void visitLineNumber(int line, Label start) {
            if (line < firstLine) {
                firstLine = line;
            }

            this.line = line;
            if (pendingMethod != null) {
                lines.put(pendingMethod, line);
                pendingMethod = null;
            }
        }

        public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
            return null;
        }

        public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
            return new LineNumberAnnotationVisitor();
        }

        public AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible) {
            return new LineNumberAnnotationVisitor();
        }

        class LineNumberMethodVisitor extends MethodVisitor {
            LineNumberMethodVisitor() {
                super(Opcodes.ASM5);
            }

            public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                return new LineNumberAnnotationVisitor();
            }

            public AnnotationVisitor visitAnnotationDefault() {
                return new LineNumberAnnotationVisitor();
            }

            public void visitFieldInsn(int opcode, String owner, String name, String desc) {
                if (opcode == Opcodes.PUTFIELD && LineNumberReader.this.name.equals(owner) && !lines.containsKey(name) && line != -1) {
                    lines.put(name, line);
                }
            }

            public void visitLineNumber(int line, Label start) {
                LineNumberReader.this.visitLineNumber(line, start);
            }
        }

        class LineNumberAnnotationVisitor extends AnnotationVisitor {
            LineNumberAnnotationVisitor() {
                super(Opcodes.ASM5);
            }

            public AnnotationVisitor visitAnnotation(String name, String desc) {
                return this;
            }

            public AnnotationVisitor visitArray(String name) {
                return this;
            }

            public void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index) {
            }

        }

    }
}
